package com.example.demo.core;

import com.example.demo.pojo.Slave;
import com.example.demo.service.SlaveService;
import com.example.demo.util.Command;
import com.example.demo.util.JsonBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

class ClientSocket {

    private final Logger log = LoggerFactory.getLogger(ClientSocket.class);

    public String getClient_name() {
        return client_name;
    }

    public String getUUID() {
        return uuid;
    }

    private String client_name;
    private String uuid;

    private final Socket socket;
    private ClientSocket(Socket socket){
        this.socket = socket;
    }

    public void sendCommand(Command command, String[] args) throws IOException {
        JsonBody body = JsonBody.custom(JsonBody.StatusCode.COMMAND);
        body.put("command", command.code());
        body.put("args", args);
        body.put("uuid", UUID.randomUUID().toString());
        OutputStream out = this.socket.getOutputStream();
        out.write(body.toJsonString().getBytes(StandardCharsets.UTF_8));
        out.flush();
    }

    public JsonBody receive(int timeout) throws IOException {
        log.info("receiving client msg...");
        this.socket.setSoTimeout(timeout);
        InputStream in = this.socket.getInputStream();
        ByteBuffer buffer = ByteBuffer.wrap(in.readNBytes(4));
        String json = new String(in.readNBytes(buffer.getInt()), StandardCharsets.UTF_8);
        log.debug("received json response from client: " + json);
        return JsonBody.toJsonBody(json);
    }

    public static ClientSocket communicate(Socket socket) throws IOException {

        ClientSocket client = new ClientSocket(socket);
        JsonBody resp;
        client.sendCommand(Command.GET_INFO, null);
        try {
            resp = client.receive(10000);
        } catch (SocketTimeoutException e){
            client.log.warn("failed communicate with remote client " + socket.getRemoteSocketAddress() + " communicate timeout in 10s");
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            client.log.warn("failed communicate with remote client " + socket.getRemoteSocketAddress());
            return null;
        }

        if (resp == null || resp.code() != JsonBody.StatusCode.SUCCESS) {
            client.log.warn("failed communicate with client, non-response or invalid response code.");
            return null;
        }
        client.client_name = (String) resp.get("client_name");
        client.uuid = (String) resp.get("client_uuid");
        return client;
    }

    public void release() throws IOException {
        this.socket.close();
    }
}

@Component
@Lazy(value = false)
public class AppServer implements IAppServer, ICommander{

    @Autowired
    SlaveService service;

    private final Logger log = LoggerFactory.getLogger(AppServer.class);
    private final ServerSocket server;
    private final Map<String, ClientSocket> clients = new ConcurrentHashMap<>();

    private IAppClientEventHandler eventHandler = new IAppClientEventHandler() {
        @Override
        public void onCommunicated(ClientSocket client) {
            log.debug("communicated hook active");
            Slave slave = new Slave();
            slave.setUuid(client.getUUID());
            slave.setClient_name(client.getClient_name());
            service.add(slave);
        }

        @Override
        public void onMessage(String uuid, ICommander commander) {

        }

        @Override
        public void onDisconnect(String uuid) {
            log.debug("disconnect hook active");
            service.remove(uuid);
        }
    };
    public AppServer() throws IOException {
        this.server = new ServerSocket(25539);
        this.server();
    }

    @Override
    public void server() {
        new Thread(() -> {
            for (;;) {
                this.doServer();
            }
        }).start();
    }

    @Override
    public void registerClientEventHandler(IAppClientEventHandler handler) {
        this.eventHandler = handler;
    }

    @Override
    public List<String> getAllClientUUID() {
        List<String> ret = new ArrayList<>();
        for (ClientSocket client : clients.values()) {
            ret.add(client.getUUID());
        }
        return ret;
    }

    private void doServer() {
        try {
            log.info("app server listen in " + this.server.getLocalSocketAddress() + ":" + this.server.getLocalPort());
            Socket socket = this.server.accept();
            if (socket.isConnected()) {

                this.communicate(socket);
                log.info("connected to client " + socket.getRemoteSocketAddress());
            }
        } catch (IOException e) {
            log.error("socket error! could not connect to client.");
            e.printStackTrace();
        }
    }

    private void communicate(Socket socket) throws IOException {
        if (!socket.isConnected()) {
            throw new IOException("attempt communicate with a unConnected socket");
        }
        ClientSocket client = ClientSocket.communicate(socket);
        if (client != null) {
            this.clients.put(client.getUUID(), client);
            if (this.eventHandler != null) { this.eventHandler.onCommunicated(client); }
        }
    }

    private void disConnectClient(String uuid) {
        ClientSocket clientSocket = this.clients.remove(uuid);
        if (clientSocket == null) { return; }
        try {
            clientSocket.release();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (this.eventHandler != null) { this.eventHandler.onDisconnect(uuid); }
        }
    }

    @Override
    public boolean sendCommand(String uuid, Command command, String[] args) throws IOException {
        ClientSocket client = this.clients.getOrDefault(uuid, null);
        if (client != null) {
            client.sendCommand(command, args);
            return true;
        }
        return false;
    }

    @Override
    public void sendCommand(String uuid, Command command, String[] args, ICommandCallback callback) {
        try {
            if (this.sendCommand(uuid, command, args)) {
                callback.onMessage(this.clients.get(uuid).receive(10000));
            }
        } catch (IOException e) {
            e.printStackTrace();
            this.disConnectClient(uuid);
            callback.onDisConnect();
        }
    }
}
